package me.yuxiaoyao.jpa.nativesql.mapper;

import me.yuxiaoyao.jpa.nativesql.annotation.NativeColumn;
import me.yuxiaoyao.jpa.nativesql.annotation.NativeEntity;
import me.yuxiaoyao.jpa.nativesql.annotation.UseCustomFieldConverter;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.util.ConcurrentReferenceHashMap;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author kerryzhang on 2020/12/16
 */


public class JpaNativeSqlConverter implements GenericConverter {
    private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(JpaNativeSqlConverter.class);

    private final Set<Class<?>> allClass;
    private final Map<String, InvokeCache> methodCache = new ConcurrentReferenceHashMap<>(2 >> 6);

    /**
     * 快速方法
     *
     * @param pkgs
     */
    public static void registerDefaultConverter(Set<String> pkgs) {
        ((DefaultConversionService) DefaultConversionService.getSharedInstance()).addConverter(new JpaNativeSqlConverter(pkgs));
    }

    public JpaNativeSqlConverter(Set<String> pkgs) {
        this.allClass = PackageUtil.getAllClassByPackage(NativeEntity.class, pkgs.toArray(new String[0]));
    }

    public JpaNativeSqlConverter(Class<?>... primaryClass) {
        Set<String> pkgs = new LinkedHashSet<>(primaryClass.length);
        for (Class<?> cls : primaryClass) {
            pkgs.add(cls.getCanonicalName());
        }
        this.allClass = PackageUtil.getAllClassByPackage(NativeEntity.class, pkgs.toArray(new String[0]));
    }

    public JpaNativeSqlConverter(String... pkgs) {
        this.allClass = PackageUtil.getAllClassByPackage(NativeEntity.class, pkgs);
    }


    public Set<Class<?>> getAllClass() {
        return allClass;
    }

    @Override
    public Set<ConvertiblePair> getConvertibleTypes() {
        if (allClass == null) {
            return Collections.emptySet();
        }
        boolean traceEnabled = logger.isTraceEnabled();
        Set<ConvertiblePair> set = new HashSet<>(allClass.size());
        for (Class<?> cls : allClass) {
            ConvertiblePair pair = new ConvertiblePair(Map.class, cls);
            set.add(pair);
            if (traceEnabled) {
                logger.trace("JPA Convert = [{}] for class = [{}]", JpaNativeSqlConverter.class, cls);
            }
        }
        return set;
    }

    @Override
    public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {

        // 单行 key 为字段名 value 为值
        @SuppressWarnings("rawtypes")
        Map map = (Map) source;

        Class<?> entityClassType = targetType.getType();

        Object o;
        try {
            // 反向创建实例
            o = ReflectUtil.newInstance(entityClassType);
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
            return null;
        }

        for (Object fieldName : map.keySet()) {
            // 列名
            String columnName = fieldName.toString();
            // set方法名
            String setMethodName = ReflectUtil.getSetMethodName(columnName);
            // cache key
            String setMethodNameCacheKey = o.getClass().getName().concat(".").concat(setMethodName);


            InvokeCache cacheMethod = methodCache.get(setMethodNameCacheKey);
            if (cacheMethod == null) {
                UseCustomFieldConverter filedFirst = entityClassType.getAnnotation(UseCustomFieldConverter.class);
                if (filedFirst != null && filedFirst.value()) {
                    // enable custom mapper
                    cacheMethod = getFieldInvokeCache(o.getClass(), columnName, InvokeType.FIELD_MAPPER);
                } else {
                    // find set 方法
                    List<Method> findMethods = ReflectUtil.getPublicMethods(targetType.getType()).stream()
                            .filter(m -> m.getName().equalsIgnoreCase(setMethodName))
                            .collect(Collectors.toList());
                    if (findMethods.size() == 1) {
                        // find set Method
                        cacheMethod = new InvokeCache(findMethods.get(0));
                    } else {
                        // use field
                        cacheMethod = getFieldInvokeCache(o.getClass(), columnName, InvokeType.FIELD);
                    }
                }
                if (cacheMethod == null) {
                    continue;
                }
                methodCache.put(setMethodNameCacheKey, cacheMethod);
            }

            if (cacheMethod.getInvokeType() == InvokeType.METHOD) {
                // method
                Method method = cacheMethod.getMethod();
                if (!Modifier.isPublic(method.getModifiers())) {
                    method.setAccessible(true);
                }
                try {
                    Object param = MapperConfiguration.mapper(map.get(fieldName), entityClassType, method.getParameterTypes()[0], null);
                    method.invoke(o, param);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException | IllegalArgumentException e) {
                    logger.error("Type not match.target = {}. fieldName = {}. fieldType = {}. sqlValueType = {}. cause: {}", targetType, fieldName, method.getParameterTypes()[0], map.get(fieldName).getClass(), e.getMessage());
                }
            } else {
                // field
                Field field = cacheMethod.getField();
                if (!Modifier.isPublic(field.getModifiers())) {
                    field.setAccessible(true);
                }
                Object param;
                if (cacheMethod.getInvokeType() == InvokeType.FIELD_MAPPER) {
                    NativeColumn annotation = field.getAnnotation(NativeColumn.class);
                    param = MapperConfiguration.mapper(map.get(fieldName), entityClassType, field.getType(), annotation.value());
                } else {
                    param = MapperConfiguration.mapper(map.get(fieldName), entityClassType, field.getType(), null);
                }
                try {
                    field.set(o, param);
                } catch (IllegalAccessException e) {
                    logger.error("Type not match.target = {}. fieldName = {}. fieldType = {}. sqlValueType = {}. cause: {}", targetType, fieldName, field.getType(), map.get(fieldName).getClass(), e.getMessage());
                }
            }
        }
        return o;
    }

    private InvokeCache getFieldInvokeCache(Class<?> cls, String columnName, InvokeType invokeType) {
        Field field = ReflectUtil.getDeclaredFieldByColumnName(cls, columnName);
        if (field == null) {
            return null;
        }
        return new InvokeCache(field, invokeType);
    }


    @SuppressWarnings("AlibabaEnumConstantsMustHaveComment")
    private enum InvokeType {
        METHOD, FIELD, FIELD_MAPPER
    }

    private static class InvokeCache {
        private final Method method;
        private final InvokeType invokeType;
        private final Field field;

        InvokeCache(Method method) {
            this.method = method;
            this.invokeType = InvokeType.METHOD;
            this.field = null;
        }

        InvokeCache(Field field, InvokeType invokeType) {
            this.method = null;
            this.invokeType = invokeType;
            this.field = field;
        }

        public Method getMethod() {
            return method;
        }

        public Field getField() {
            return field;
        }

        public InvokeType getInvokeType() {
            return invokeType;
        }
    }
}
